Skip to content

feat: generic ecs material with texture type slots + shader uniforms#33

Merged
gimploo merged 18 commits into
devfrom
feat/material-texture-pipeline
Jul 10, 2026
Merged

feat: generic ecs material with texture type slots + shader uniforms#33
gimploo merged 18 commits into
devfrom
feat/material-texture-pipeline

Conversation

@gimploo

@gimploo gimploo commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the hardcoded render-system uniform layout with a material-driven approach, enabling different models to use different shaders with different uniform/texture layouts.

Built on top of #32 (embedded texture loading via materials).

Changes

1. ecs/component/types.h — Extended ecs_component_material_t

struct ecs_component_material_t {
    struct {
        u32 asset_ids[GL_TEXTURE_TYPE_COUNT];  // texture asset IDs by type
    } texture;
    struct {
        u32 asset_id;                           // shader asset ID
        gluniforms_t uniforms;                  // per-material uniform overrides
    } shader;
};

2. ecs/systems/model.h — Material-driven render system

  • Reads shader from material->shader.asset_id
  • Resolves texture asset IDs from material->texture.asset_ids[] to GL handles
  • Binds model textures from glmodel_get_texuturelist() automatically
  • Copies material uniforms to rendercommand
  • Auto-injects per-frame uniforms: view, projection, transform, material.color
  • Auto-injects uBones if skeleton present
  • Auto-injects legacy light uniforms for backward compat

3. ecs/component.h — Capped material pool capacity

Added ECS_CMP_MATERIAL: return 128 case — prevents OOM since the new struct is ~1.4KB (was 8 bytes). Also increased the component copy buffer from WORD to 3*KB.

4. ecs/systems/mesh.h — Updated field access

material->shader_asset_idmaterial->shader.asset_id

5. ecs/serialization.h — Updated material field map

Serializes shader.asset_id as single field (texture arrays and uniforms are runtime-only, reconstructed from assets on load).

Testing

  • Full application build compiles
  • Runs without crashes, no memory leaks
  • Existing player model (male-model-shader) renders correctly with backward-compat uniforms

gimploo added 6 commits July 10, 2026 10:59
Replace hardcoded render-system uniforms with material-driven approach:
- ecs_component_material_t now stores shader.asset_id + shader.uniforms
  and texture.asset_ids[GL_TEXTURE_TYPE_COUNT] typed array
- ecs_system_render_model reads material uniforms and resolves
  texture asset IDs to GL handles at render time
- Model textures from glmodel_get_texuturelist also bound automatically
- Auto-injected uniforms: view, projection, transform (per-frame),
  material.color, uBones (if skeleton present)
- Updated mesh.h and serialization.h for new struct field layout
@gimploo gimploo force-pushed the feat/material-texture-pipeline branch from b42dd98 to ed6baf6 Compare July 10, 2026 05:29
Comment thread gfx/model/assimp.h Outdated

//load all animations
animator_load_all_animations(&o.animator, scene, o.arena, (animation_filter_t){0});
animator_load_all_animations(&o.animator, scene, o.arena);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert this

gimploo added 2 commits July 10, 2026 11:03
rendercommand_t.material.texture is now a gltexturelist_t instead of
flat u16 ids[], carrying per-texture type metadata through the entire
render queue — from command submission to batch comparison to binding.

Changes:
- render_command.h: rendercommand material uses gltexturelist_t texture
- render_command.h: compare_textures compares type + GL texture id
- render_queue.h: dispatch binds via gltextureitem_t (2D or cubemap)
- model.h: directly assign glmodel_get_texuturelist() into material,
  resolve per-type material texture asset IDs into typed items
- mesh.h: construct gltexturelist_t for atlas texture
Comment thread ecs/component/types.h Outdated
Comment on lines +103 to +105
struct {
u32 asset_ids[GL_TEXTURE_TYPE_COUNT];
} texture;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change this to gltexturelist_t textures;

Comment thread ecs/systems/model.h Outdated
Comment on lines +93 to +148
gluniforms_t *uniforms = &cmd.material.shader.uniforms;

uniforms->data[uniforms->count].name = str("view");
uniforms->data[uniforms->count].value.mat4 = glcamera_getview(ctx.active_camera);
uniforms->count++;

uniforms->data[uniforms->count].name = str("projection");
uniforms->data[uniforms->count].value.mat4 = perspective_projection;
uniforms->count++;

uniforms->data[uniforms->count].name = str("transform");
uniforms->data[uniforms->count].value.mat4 = model_transform;
uniforms->count++;

uniforms->data[uniforms->count].name = str("material.color");
uniforms->data[uniforms->count].value.vec4 = *(vec4f_t *)list_get_value(&model->colors, idx);
uniforms->count++;

if (model->transforms[idx].len) {
uniforms->data[uniforms->count].name = str("uBones");
uniforms->data[uniforms->count].value.mat4s.count = model->transforms[idx].len;
uniforms->data[uniforms->count].value.mat4s.data = (matrix4f_t *)model->transforms[idx].data;
uniforms->count++;
}

for (u8 u = 0; u < material->shader.uniforms.count; u++) {
uniforms->data[uniforms->count] = material->shader.uniforms.data[u];
uniforms->count++;
}

uniforms->data[uniforms->count].name = str("light.color");
uniforms->data[uniforms->count].value.vec4 = global_workbench->editor.current_selected_entity_id == entry->entity_id ? COLOR_RED : COLOR_WHITE;
uniforms->count++;

uniforms->data[uniforms->count].name = str("light.ambient");
uniforms->data[uniforms->count].value.f32 = 1.0f;
uniforms->count++;

uniforms->data[uniforms->count].name = str("light.position");
uniforms->data[uniforms->count].value.vec3 = vec3f(1.0f);
uniforms->count++;

cmd.material.texture = model_textures;

gltexturelist_t *texlist = &cmd.material.texture;
for (u8 t = 0; t < GL_TEXTURE_TYPE_COUNT; t++) {
if (!material->texture.asset_ids[t]) continue;
gltexture2d_t *tex = (gltexture2d_t *)assetmanager_get_assetresource(
&global_engine->systems.assets, ASSET_TYPE_TEXTURE, material->texture.asset_ids[t]);
if (tex && texlist->count < MAX_SUPPORTED_TEXTURE_COUNT_PER_DRAW_CALL) {
texlist->items[texlist->count++] = (gltextureitem_t){
.type = (gltexturetype)t,
.source = { .normal_texture = tex }
};
}
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this done this way - we have the asset ids for the textures with us - also is there a better approach to the uniform assignment - maybe a regulator in bw the checks for these uniforms - and then update them as per how its ocnifured this would also makes it easier to bring about a consistent naming for the uniforms

gimploo added 3 commits July 10, 2026 11:15
Replaces hardcoded uniform injection blocks with a static registry
table mapping uniform names to value sources:

- ecs/uniform_registry.h: UNIFORM_REGISTRY[] table with 8 entries
  (view, projection, transform, material.color, uBones, light.*)
  plus uniform_registry_lookup(), uniform_registry_compute(),
  and uniform_registry_apply_material_overrides()

- model.h / mesh.h: iterate shader->internal.uniformlocs,
  resolve each name via registry, eprint() crash on unknowns.
  Material.shader.uniforms overrides registry defaults.
Comment thread ecs/systems/mesh.h Outdated
Comment on lines +90 to +107

gluniforms_t *uniforms = &command.material.shader.uniforms;

hashtable_iterator(&shader->internal.uniformlocs, loc_entry)
{
const hashtable_entry_t *he = loc_entry;
const str_t uniform_name = he->key.str;
const uniform_binding_t *binding = uniform_registry_lookup(uniform_name);
if (!binding)
eprint("uniform '%.*s' not found in registry for shader '%.*s'", uniform_name.len, uniform_name.data, shader->vs.len, shader->vs.data);

uniforms->data[uniforms->count].name = uniform_name;
uniforms->data[uniforms->count].value = uniform_registry_compute(binding->source, &uniform_ctx);
uniforms->count++;
}

uniform_registry_apply_material_overrides(uniforms, &material->shader.uniforms);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this prior to render command initalization - look if it can be assigned to the command member

Comment thread ecs/systems/model.h Outdated
Comment on lines +72 to +75
uniform_ctx.model_color = *(vec4f_t *)list_get_value(&model->colors, idx);
uniform_ctx.bones.count = model->transforms[idx].len;
uniform_ctx.bones.data = (matrix4f_t *)model->transforms[idx].data;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these set this way - not all models would have bones right ?

…r static bones

- model.h / mesh.h: material.shader.uniforms overrides checked inline
  during registry iteration, removed standalone apply function
- uniform_registry.h: UNIFORM_SOURCE_BONE_TRANSFORMS returns single
  identity matrix when bones.count == 0 (static models)
- Removed now-unused uniform_registry_apply_material_overrides()
Comment thread ecs/serialization.h
.fields = {
CMP_FLD_DESC("\ttexture_asset_id:", "\ttexture_asset_id:%u", ecs_component_material_t, texture_asset_id, ECS_CMP_FLD_U32),
CMP_FLD_DESC("\tshader_asset_id:", "\tshader_asset_id:%u", ecs_component_material_t, shader_asset_id, ECS_CMP_FLD_U32),
CMP_FLD_DESC("\tshader_asset_id:", "\tshader_asset_id:%u", ecs_component_material_t, shader.asset_id, ECS_CMP_FLD_U32),

@gimploo gimploo Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks wrong ?

Comment thread ecs/component.h
const u16 cmp_size = ecs_component__internal_get_componenttype_size(cmp_type);

buffer(WORD) buf = {0};
buffer(KB * 3) buf = {0};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does it need 3KB ?

Comment thread ecs/uniform_registry.h Outdated
Comment on lines +28 to +38
typedef struct {
const glcamera_t *active_camera;
f32 aspect_ratio;
const ecs_component_transform_t *transform;
vec4f_t model_color;
struct {
u32 count;
matrix4f_t *data;
} bones;
bool is_selected;
} uniform_compute_ctx_t;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think this should be here ? - this can be realsed during renderpass time

Comment thread ecs/uniform_registry.h Outdated
{ str_lit("light.position"), GL_UNIFORM_TYPE_VEC3F, UNIFORM_SOURCE_LIGHT_POSITION },
};
#define UNIFORM_REGISTRY_COUNT ARRAY_LEN(UNIFORM_REGISTRY)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont use static use INTERNAL macro for the entire file

Comment thread ecs/uniform_registry.h Outdated
Comment on lines +60 to +106
static gluniform_value_t uniform_registry_compute(uniform_source_t source, const uniform_compute_ctx_t *ctx)
{
switch (source)
{
case UNIFORM_SOURCE_CAMERA_VIEW:
if (ctx->active_camera) return (gluniform_value_t){ .mat4 = glcamera_getview(ctx->active_camera) };
break;

case UNIFORM_SOURCE_CAMERA_PROJECTION:
return (gluniform_value_t){ .mat4 = glms_perspective(radians(45), ctx->aspect_ratio, 1.0f, 1000.0f) };

case UNIFORM_SOURCE_ENTITY_TRANSFORM:
if (ctx->transform)
return (gluniform_value_t){ .mat4 = glms_mat4_mul(
glms_translate_make(ctx->transform->position),
glms_mat4_mul(
glms_quat_mat4(ctx->transform->orientation),
glms_scale_make(ctx->transform->scale)
)
)};
break;

case UNIFORM_SOURCE_MODEL_COLOR:
return (gluniform_value_t){ .vec4 = ctx->model_color };

case UNIFORM_SOURCE_BONE_TRANSFORMS:
if (ctx->bones.count > 0)
return (gluniform_value_t){ .mat4s = { .count = ctx->bones.count, .data = ctx->bones.data } };
{
static matrix4f_t identity = GLMS_MAT4_IDENTITY_INIT;
return (gluniform_value_t){ .mat4s = { .count = 1, .data = &identity } };
}

case UNIFORM_SOURCE_LIGHT_COLOR:
return (gluniform_value_t){ .vec4 = ctx->is_selected ? COLOR_RED : COLOR_WHITE };

case UNIFORM_SOURCE_LIGHT_AMBIENT:
return (gluniform_value_t){ .f32 = 1.0f };

case UNIFORM_SOURCE_LIGHT_POSITION:
return (gluniform_value_t){ .vec3 = (vec3f_t){1.0f, 1.0f, 1.0f} };

case UNIFORM_SOURCE_COUNT:
break;
}
return (gluniform_value_t){0};
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most probably whats going to happen is that the ecs would have all this value required right ? - so instead remove this and have the depending on what uniformregistry need the shader requires, we provide those values to the renderpass shader memeber straight way assigning to the buffer.

Comment thread ecs/serialization.h
Comment on lines 381 to -382
} break;
case ECS_CMP_MATERIAL: {
ecs_component_material_t *mat = &bundle->component[cmp_idx].material;
if (mat->texture_asset_id) mat->texture_asset_id = ecs_deserializer__internal_ensure_asset_loaded(assets, mat->texture_asset_id, assets_parsed);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this removed ?

…AL macro

- uniform_registry.h: removed uniform_compute_ctx_t and
  uniform_registry_compute(); registry is now pure lookup table.
  Functions use INTERNAL macro instead of static.
- model.h / mesh.h: inline value computation via switch(binding->source)
  directly from local data (camera, transform, model, lights).
  Material overrides still applied inline.
- component.h: comment justifying 3KB buffer for material component.
Comment thread ecs/systems/model.h Outdated
Comment on lines +101 to +153
hashtable_iterator(&shader->internal.uniformlocs, loc_entry)
{
const hashtable_entry_t *he = loc_entry;
const str_t uniform_name = he->key.str;
const uniform_binding_t *binding = uniform_registry_lookup(uniform_name);
if (!binding)
eprint("uniform '%.*s' not found in registry for shader '%.*s'", uniform_name.len, uniform_name.data, shader->vs.len, shader->vs.data);

gluniform_value_t value = {0};
switch (binding->source)
{
case UNIFORM_SOURCE_CAMERA_VIEW:
value.mat4 = view_mat;
break;
case UNIFORM_SOURCE_CAMERA_PROJECTION:
value.mat4 = proj;
break;
case UNIFORM_SOURCE_ENTITY_TRANSFORM:
value.mat4 = entity_transform;
break;
case UNIFORM_SOURCE_MODEL_COLOR:
value.vec4 = mesh_color;
break;
case UNIFORM_SOURCE_BONE_TRANSFORMS:
if (bone_count > 0) {
value.mat4s.count = bone_count;
value.mat4s.data = bone_data;
} else {
static matrix4f_t identity = GLMS_MAT4_IDENTITY_INIT;
value.mat4s.count = 1;
value.mat4s.data = &identity;
}
break;
case UNIFORM_SOURCE_LIGHT_COLOR:
value.vec4 = is_selected ? COLOR_RED : COLOR_WHITE;
break;
case UNIFORM_SOURCE_LIGHT_AMBIENT:
value.f32 = 1.0f;
break;
case UNIFORM_SOURCE_LIGHT_POSITION:
value.vec3 = (vec3f_t){1.0f, 1.0f, 1.0f};
break;
case UNIFORM_SOURCE_COUNT:
break;
}

for (u8 o = 0; o < material->shader.uniforms.count; o++)
if (str_cmp(material->shader.uniforms.data[o].name, uniform_name))
value = material->shader.uniforms.data[o].value;

uniforms->data[uniforms->count].name = uniform_name;
uniforms->data[uniforms->count].value = value;
uniforms->count++;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do the inverse loop through all uniform names and then look it up in the shader - if not found crash using eprin

Comment thread ecs/systems/mesh.h Outdated
…nknowns

Move material.shader.uniforms overrides to a dedicated pass after
shader uniform population (instead of inline during iteration).
Each material uniform is looked up in the populated list; unknown
names trigger eprint() crash, catching accidental binding typos.
Comment thread ecs/systems/mesh.h Outdated
- Registry: LIGHT_COLOR/AMBIENT/POSITION sources replaced by
  single UNIFORM_SOURCE_MATERIAL — material.shader.uniforms
  is the only source for these values, crash on missing
- model.h / mesh.h: switch handles MATERIAL case inline,
  reads value from material component. Removed 20-line
  separate override pass from both files.
- model.h: dropped unused is_selected variable
Comment thread ecs/systems/model.h Outdated
Comment on lines 136 to 145
for (u8 o = 0; o < material->shader.uniforms.count; o++) {
if (str_cmp(material->shader.uniforms.data[o].name, uniform_name)) {
value = material->shader.uniforms.data[o].value;
found = true;
break;
}
}
if (!found)
eprint("material uniform '%.*s' not set for shader '%.*s'", uniform_name.len, uniform_name.data, shader->vs.len, shader->vs.data);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard code the assignments - i dont see these changes if its in the uniform registry

Comment thread ecs/serialization.h
case ECS_CMP_MATERIAL: {
ecs_component_material_t *mat = &bundle->component[cmp_idx].material;
if (mat->texture_asset_id) mat->texture_asset_id = ecs_deserializer__internal_ensure_asset_loaded(assets, mat->texture_asset_id, assets_parsed);
if (mat->shader_asset_id) mat->shader_asset_id = ecs_deserializer__internal_ensure_asset_loaded(assets, mat->shader_asset_id, assets_parsed);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure tests are updated for ecs serialization

Comment thread ecs/systems/mesh.h Outdated

gluniforms_t *uniforms = &command.material.shader.uniforms;

hashtable_iterator(&shader->internal.uniformlocs, loc_entry)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this into a separate function that have it staright away assinged to rendercommand

gimploo added 3 commits July 10, 2026 17:35
Remove ecs/uniform_registry.h entirely. Each render system now
writes its uniforms directly into the rendercommand — no lookup
table, no source enum, no shared abstraction.

model.h: view, projection, transform, material.color, uBones
(conditional), material uniforms dumped wholesale.
mesh.h: view, projection (instance shader baseline), material
uniforms dumped wholesale.

Removes ~100 lines of abstraction for no real loss at this scale.
New ecs_system_uniform_resolve runs before render systems
and pre-populates material->shader.uniforms with per-frame
values (view, projection, transform) from camera + entity data.
Only fills uniforms the material hasn't already set.

- ecs/uniform_registry.h: reinstated with resolve function
- ecs/systems/uniform_resolve.h: new ECS system, iterates
  ECS_CMP_MATERIAL pool, resolves uniforms per entity
- ecs/system.h: resolver registered after collider, before animation

Render systems simplified:
- model.h: copies resolved uniforms, adds only per-mesh
  material.color + uBones
- mesh.h: copies resolved uniforms directly, zero uniform math
- test save files: material {totalmembers:1,bytesize:...} with
  only shader_asset_id field (texture_asset_id removed)
- test source: updated ecs_component_material_t initialization
  and assertions to use shader.asset_id
- build script: fixed include path for standalone worktree builds
@gimploo gimploo merged commit c93e5c7 into dev Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant